Skip to content

S13-04 React-Redux

[TOC]

API

Store

store是Redux中的一个对象,表示整个应用程序的状态存储

  • createStore(reducer, preloadedState?, enhancer? )返回:store,用于创建Redux store的函数

    • 参数
    • reducer(state, action) => void,用于处理Redux应用程序状态的函数,并在action被dispatch时更新状态
    • preloadedStateObject,用于初始化Redux应用程序状态的对象
    • enhancerFunction,增强Redux store功能的函数,比如使用中间件、使用调试工具等
    • 返回
    • storeObject,store是一个存储应用程序状态的对象,可以帮助管理应用程序的状态,并在状态变化时更新UI。
  • store.getState(void)返回:,获取当前的状态树。

  • store.dispatch(action)返回:,用于触发状态变化。它接受一个表示状态变化的action对象作为参数,并将该对象传递给Redux的reducer函数,从而更新应用程序的状态。

    • 参数
    • action{ type },一个普通的JavaScript对象,用于表示状态变化的动作。它必须包含一个type属性,用于指定状态变化的类型
  • store.subscribe(listener)返回:unsubscribe,用于订阅状态变化。它接受一个回调函数作为参数,该回调函数会在每次状态发生变化时被调用。

    • 参数
    • listener() => void, 回调函数,用于处理状态变化的逻辑。
    • 返回
    • unsubscribe() => void,取消订阅的函数,调用该方法取消订阅: unsubscribe()
  • combineReducers(reducers)返回:reducer,用于合并多个reducer的函数

    • 参数
    • reducers{reducer,...},一个由多个reducer组成的对象,每个reducer都是一个函数,用于处理不同的state
    • 返回
    • reducer(state, action) => void,合并后的reducer函数

React-Redux

React-Redux是一个将React和Redux结合起来使用的库,它提供了一种将Redux的状态管理React组件结合起来的方式。

  • <Provider>返回:,作用是将Redux store注入到React应用中,使得所有的组件都可以访问到store中的状态。
    • 属性
    • store<Provider store={store}> <App/> </Provider>,将store作为Provider组件的props传入
  • connect(mapStateToProps, mapDispatchToProps?)(Cpn)返回:,作用是将React组件和Redux store连接起来。通过connect()函数,React组件可以访问到store中的状态,并且在状态变化时自动更新组件。
    • 参数
    • mapStateToProps(state) => void,将Redux store中的状态映射到组件的props中
    • mapDispatchToProps(dispatch) => void,将dispatch方法映射到组件的props中
    • Cpn组件,被连接的React组件

Redux Toolkit

Redux Toolkit的核心API主要是如下几个:

  • configureStore({ reducer, middleware, devTools, ... })返回:store,包装createStore以提供简化的配置选项和良好的默认值。它可以自动组合你的 slice reducer,添加你提供的任何 Redux 中间件,redux-thunk默认包含,并启用 Redux DevTools Extension。
    • 参数
    • reducer:``,Redux store 的根 reducer
    • middleware:``,要使用的中间件数组
    • devTools:``,是否启用开发工具(如 Redux DevTools),默认true
    • preloadedState:``,初始状态
    • enhancers:``,其他 store 增强器
    • 返回
    • store:``,返回的是一个 Redux store 实例,而不是一个类。因此无法创建多个 store 实例
  • createSlice({ name, initialState, reducers,... })返回:reducerSlice,用于创建一个Redux reducer和action creator的集合
    • 参数
    • nameString,用于标识这个reducer的名称,action.type会根据name生成
    • initialStateany,表示这个reducer的初始状态
    • reducers{ reducer,... },用于定义这个reducer的action creator和对应的reducer函数
      • reducer(state, action) => void,相当于之前的reducer函数
    • extraReducers: { reducer,... },包含了与当前slice无关的reducer函数
      • reducer(state, action) => void,相当于之前的reducer函数
    • 返回
    • reducerSlice:``,返回一个reducer片段
  • createAsyncThunk(typePrefix, payloadCreator, options? )返回:,用于创建一个异步action creator
    • 参数
    • typePrefixString,用于标识这个异步action creator的类型前缀
    • payloadCreator(arg, thunkAPI) => Promise,用于处理异步操作并返回一个Promise对象
    • options?Object,用于配置异步action creator的一些选项
      • fulfilled:用于指定异步操作成功时的处理函数。
      • rejected:用于指定异步操作失败时的处理函数。
      • pending:用于指定异步操作进行中时的处理函数。
      • dispatchCondition:用于指定在什么条件下才会dispatch这个action的函数。
      • condition:用于指定在什么条件下才会调用payloadCreator函数的函数。
      • typeSuffixes:用于指定异步action creator的类型后缀的对象。
      • serializeError:用于指定如何序列化异步操作的错误信息的函数。

image-20230414182149658

Redux核心思想

理解JavaScript纯函数

函数式编程中有一个非常重要的概念叫纯函数,JavaScript符合函数式编程的范式,所以也有纯函数的概念

  • 在react开发中纯函数是被多次提及的;

  • 比如react中组件就被要求像是一个纯函数(为什么是像,因为还有class组件),redux中有一个reducer的概念,也是要求必须是一个纯函数

  • 所以掌握纯函数对于理解很多框架的设计是非常有帮助的;

纯函数的维基百科定义:

  • 在程序设计中,若一个函数符合以下条件,那么这个函数被称为纯函数:

  • 此函数在相同的输入值时,需产生相同的输出。

  • 函数的输出和输入值以外的其他隐藏信息或状态无关,也和由I/O设备产生的外部输出无关。

  • 该函数不能有语义上可观察的函数副作用,诸如“触发事件”,使输出设备输出,或更改输出值以外物件的内容等。

当然上面的定义会过于的晦涩,所以我简单总结一下:

  • 确定的输入,一定会产生确定的输出

  • 函数在执行过程中,不能产生副作用;(如触发事件)

副作用概念的理解

那么这里又有一个概念,叫做副作用,什么又是副作用呢?

  • *副作用(side effect)*其实本身是医学的一个概念,比如我们经常说吃什么药本来是为了治病,可能会产生一些其他的副作用;

  • 在计算机科学中,也引用了副作用的概念,表示在执行一个函数时,除了返回函数值之外,还对调用函数产生了附加的影响,比如修改了全局变量,修改参数或者改变外部的存储

纯函数在执行的过程中就是不能产生这样的副作用:

  • 副作用往往是产生bug的 “温床”

纯函数的案例

我们来看一个对数组操作的两个函数:

  • slice:slice 截取数组时不会对原数组进行任何操作,而是生成一个新的数组;

  • splice:splice截取数组, 会返回一个新的数组, 也会对原数组进行修改;

slice就是一个纯函数,不会修改数组本身,而splice函数不是一个纯函数;

js
    const arr = ['Tom', 'Jack', 'Jerry', 'John']

    // 1. slice 不会修改原数组,并返回被删除的元素,是纯函数
    const res1 = arr.slice(1, 2)
    console.log('arr: ', arr, 'res1: ', res1) // arr:  (4) ['Tom', 'Jack', 'Jerry', 'John'] res1:  ['Jack']

    // 2. splice 会修改原数组,并返回被删除的元素
    const res2 = arr.splice(1, 1)
    console.log('arr: ', arr, 'res2: ', res2) // arr:  (3) ['Tom', 'Jerry', 'John'] res2:  ['Jack']

判断下面函数是否是纯函数?

是纯函数

js
    // 纯函数
    function foo (num1, num2) {
      return num1 + num2
    }

不是纯函数

js
    // 2. 不是纯函数
+    let x = 10
    function bar(num) {
+      return num + x
    }
    console.log(bar(20))
    x = 20
    console.log(bar(20))

不是纯函数

js
    // 3. 不是纯函数
    const info = { name: 'Jack', age: 10 }
    function baz(info) {
+      info.name = 'Tom'
    }
    baz(info)
    console.log(info)

纯函数的作用和优势

为什么纯函数在函数式编程中非常重要呢?

  • 因为你可以安心的编写和安心的使用

  • 你在写的时候保证了函数的纯度,只是单纯实现自己的业务逻辑即可,不需要关心传入的内容是如何获得的或者依赖其他的外部变量是否已经发生了修改;

  • 你在用的时候,你确定你的输入内容不会被任意篡改,并且自己确定的输入,一定会有确定的输出;

React中就要求我们无论是函数还是class声明一个组件,这个组件都必须像纯函数一样,保护它们的props不被修改:

在接下来学习redux中,reducer也被要求是一个纯函数。

为什么需要redux

JavaScript开发的应用程序,已经变得越来越复杂了:

  • JavaScript需要管理的状态越来越多,越来越复杂;

  • 这些状态包括服务器返回的数据缓存数据用户操作产生的数据等等,也包括一些UI的状态,比如某些元素是否被选中,是否显示加载动效,当前分页;

管理不断变化的state是非常困难的:

  • 状态之间相互会存在依赖,一个状态的变化会引起另一个状态的变化,View页面也有可能会引起状态的变化;

  • 当应用程序复杂时,state在什么时候,因为什么原因而发生了变化,发生了怎么样的变化,会变得非常难以控制和追踪;

React是在视图层帮助我们解决了DOM的渲染过程,但是State依然是留给我们自己来管理:

  • 无论是组件定义自己的state,还是组件之间的通信通过props进行传递;也包括通过Context进行数据之间的共享;

  • React主要负责帮助我们管理视图state如何维护最终还是我们自己来决定

image-20230404111336581

Redux就是一个帮助我们管理State的容器:Redux是JavaScript的状态容器,提供了可预测的状态管理;

Redux除了和React一起使用之外,它也可以和其他界面库一起来使用(比如Vue),并且它非常小(包括依赖在内,只有2kb

Redux的核心理念 - Store

Redux的核心理念非常简单。

比如我们有一个朋友列表需要管理:

  • 如果我们没有定义统一的规范来操作这段数据,那么整个数据的变化就是无法跟踪的;

  • 比如页面的某处通过products.push的方式增加了一条数据;

  • 比如另一个页面通过products[0].age = 25修改了一条数据;

整个应用程序错综复杂,当出现bug时,很难跟踪到底哪里发生的变化;

image-20230404111437002

Redux的核心理念 - action

Redux要求我们通过action来更新数据

  • 所有数据的变化,必须通过派发(dispatch)action来更新

  • action是一个普通的JavaScript对象,用来描述这次更新的typecontent

比如下面就是几个更新friends的action:

  • 强制使用action的好处是可以清晰的知道数据到底发生了什么样的变化,所有的数据变化都是可跟追、可预测的;

  • 当然,目前我们的action是固定的对象;

  • 真实应用中,我们会通过函数来定义,返回一个action;

image-20230404111446672

Redux的核心理念 - reducer

但是如何将state和action联系在一起呢?答案就是reducer

  • reducer是一个纯函数

  • reducer做的事情就是将传入的state和action结合起来生成一个新的state

image-20230404111514037

Redux基本使用

Redux测试项目搭建

安装redux:

sh
npm install redux --save
# 或
yarn add redux

1.创建一个新的项目文件夹:learn-redux

sh
# 执行初始化操作
yarn init
# 安装redux
yarn add redux

2.创建src目录,并且创建index.js文件

3.修改package.json可以执行index.js

js
"scripts": {
  "start": "node src/index.js"
}

Redux的基本使用

1、创建一个对象,作为我们要保存的状态:

js
// 1. state对象保存状态 
const initialState = {
  name: 'Tom',
  age: 10
}

2、创建Store来存储这个state

  • 创建store时必须创建reducer;

    js
      import { createStore } from 'redux'
      // 1. state对象保存状态 
      const initialState = {
        name: 'Tom',
        age: 10
      }
    
      // 2. reducer纯函数
    +  function reducer(state = initialState, action) {
        return state
      }
    
      // 3. 创建store
    +  const store = createStore(reducer)
    
      export default store
  • 我们可以通过 store.getState 来获取当前的state;

    js
      import store from './store'
      // 4. 获取store中的state
    +  console.log(store.getState()) // {name: 'Tom', age: 10}

3、通过action来修改state

  • 通过dispatch来派发action;

  • 通常action中都会有type属性,也可以携带其他的数据;

js
    // 5. 修改store
    const nameAction = { type: 'change_name', name: '张飞' }
    store.dispatch(nameAction)
    console.log(store.getState()) // {name: '张飞', age: 10}

4、修改reducer中的处理代码

  • 这里一定要记住,reducer是一个纯函数,不要直接修改state;

  • 后面我会讲到直接修改state带来的问题;

js
  /**
   * 通过reducer函数修改state状态
   * @param {Object} state store中目前保存的state
   * @param {Object} action 本次需要更新的action,通过dispatch传入
   * @returns {Object} state State对象
   */
  function reducer(state = initialState, action) {
    switch (action.type) {
      // 有数据更新
      case 'change_name':
        return { ...state, name: action.name }
      // 没有数据更新
      default:
        return state
    }
  }

5、可以在派发action之前,监听store的变化:

订阅store中的数据

js
    // 6. 订阅(监听)store中的数据
    store.subscribe(() => {
      console.log('subscribe: ', store.getState()) // {name: '张飞', age: 10}
    })

取消订阅

js
    // 6. 订阅(监听)store中的数据
    const unsubscribe = store.subscribe(() => {
      console.log('subscribe: ', store.getState()) // {name: '张飞', age: 10}
    })
    // 取消订阅
+    unsubscibe()

6.优化: 使用switch 代替 if else

js
  function reducer(state = initialState, action) {
+    switch (action.type) {
      case 'change_name':
        return { ...state, name: action.name }
      default:
        return state
    }
  }

7.优化: 动态生成action

js
    // 7. 优化: 动态生成action
    function changeAge(age) {
      return {
        type: 'change_age',
        age
      }
    }
    store.dispatch(changeAge(33)) // => {name: '张飞', age: 33}

**8.优化: 创建独立的action文件:actionCreators.js **

创建:

js
import { CHANGE_NAME, CHANGE_AGE } from './constants'

export function changeName(name) {
  return {
    type: CHANGE_NAME,
    name
  }
}
 
export default function changeAge(age) {
  return {
    type: CHANGE_AGE,
    age
  }
}

引入使用:

js
import { changeAge, changeName } from './store/actionCreators'

store.dispatch(changeName('刘备'))
store.dispatch(changeAge(55))

**9.优化: 创建独立的常量文件:constants.js **

定义常量:

js
export const CHANGE_NAME = 'change_name'
export const CHANGE_AGE = 'change_age'

使用常量:

actionCreator.js

js
+ import { CHANGE_NAME, CHANGE_AGE } from './constants'

export function changeName(name) {
  return {
+    type: CHANGE_NAME,
    name
  }
}
 
export function changeAge(age) {
  return {
+    type: CHANGE_AGE,
    age
  }
}

reducer.js

js
+ import { CHANGE_NAME, CHANGE_AGE } from './constants'
const initialState = {
  name: 'Tom',
  age: 10
}
export default function reducer(state = initialState, action) {
  switch (action.type) {
+    case CHANGE_NAME:
      return { ...state, name: action.name }
+    case CHANGE_AGE: 
      return { ...state, age: action.age }
    default:
      return state
  }
}

10.优化: 创建独立的reducer文件:reducer.js

定义reducer:

js
import { CHANGE_NAME, CHANGE_AGE } from './constants'

const initialState = {
  name: 'Tom',
  age: 10
}

export default function reducer(state = initialState, action) {
  switch (action.type) {
    case CHANGE_NAME:
      return { ...state, name: action.name }
    case CHANGE_AGE: 
      return { ...state, age: action.age }
    default:
      return state
  }
}

使用reducer:

js
import { createStore } from 'redux'
+ import reducer from './reducer'

+ const store = createStore(reducer)

export default store

Redux结构划分

如果我们将所有的逻辑代码写到一起,那么当redux变得复杂时代码就难以维护。

  • 接下来,我会对代码进行拆分,将store、reducer、action、constants拆分成一个个文件。

  • 创建store/index.js文件:

  • 创建store/reducer.js文件:

  • 创建store/actionCreators.js文件:

  • 创建store/constants.js文件:

注意:node中对ES6模块化的支持

  • 目前我使用的node版本是v12.16.1,从node v13.2.0开始,node才对ES6模块化提供了支持:

  • node v13.2.0之前,需要进行如下操作:

    • 在package.json中添加属性: "type": "module";

    • 在执行命令中添加如下选项:node --experimental-modules src/index.js;

  • node v13.2.0之后,只需要进行如下操作:

    • 在package.json中添加属性: "type": "module";

注意:导入文件时,需要跟上.js后缀名;

Redux的三大原则

单一数据源

  • 整个应用程序的state被存储在一颗object tree中,并且这个object tree只存储在一个 store 中

  • Redux并没有强制让我们不能创建多个Store,但是那样做并不利于数据的维护;

  • 单一的数据源可以让整个应用程序的state变得方便维护、追踪、修改

State是只读

  • 唯一修改State的方法一定是触发action,不要试图在其他地方通过任何的方式来修改State:

  • 这样就确保了View或网络请求都不能直接修改state,它们只能通过action来描述自己想要如何修改state;

  • 这样可以保证所有的修改都被集中化处理,并且按照严格的顺序来执行,所以不需要担心race condition(竟态)的问题;

使用纯函数来执行修改

  • 通过reducer将 旧state和 actions联系在一起,并且返回一个新的State:

  • 随着应用程序的复杂度增加,我们可以将reducer拆分成多个小的reducers,分别操作不同state tree的一部分;

  • 但是所有的reducer都应该是纯函数,不能产生任何的副作用

Redux使用流程

我们已经知道了redux的基本使用过程,那么我们就更加清晰来认识一下redux在实际开发中的流程:

image-20230404111713118

Redux官方图

image-20230404111723533

React结合Redux

redux融入react代码

目前redux在react中使用是最多的,所以我们需要将之前编写的redux代码,融入到react当中去。

这里我创建了两个组件:

  • Home组件:其中会展示当前的counter值,并且有一个+1和+5的按钮;

  • Profile组件:其中会展示当前的counter值,并且有一个-1和-5的按钮;

image-20230414133811515

核心代码主要是两个:

  • 在 componentDidMount 中订阅数据的变化,当数据发生变化时重新设置 counter;

  • 在发生点击事件时,调用store的dispatch来派发对应的action;

示例

1、页面布局

image-20230410175855615

js
import './App.css'
import Home from './cpns/Home'
import Profile from './cpns/Profile'

export class App extends PureComponent {
  render() {
    return (
      <div>
        <h3>App Counter: 0</h3>
        <div className="main">
+          <Home />
+          <Profile />
        </div>
      </div>
    )
  }
}
export default App

2、安装redux

sh
npm i redux

3、创建Store

store/index.js

js
import { createStore } from "redux";
import counterReducer from './reducer'

const store = createStore(counterReducer)

export default store

store/reducer.js

js
import { ADD_COUNTER, SUB_COUNTER } from './constants'

const initialState = {
  counter: 100
}

export default function reducer(state = initialState, action) {
  switch (action.type) {
    case ADD_COUNTER:
+      return { ...state, counter: state.counter + action.counter } 
    case SUB_COUNTER: 
+      return { ...state, counter: state.counter - action.counter }
    default:
      return state
  }
}

3、展示Store数据

js
export class App extends PureComponent {
  constructor() {
    super()
    this.state = {
+      counter: store.getState().counter
    }
  }
  componentDidMount() {
+    store.subscribe(() => {
+      this.setState({ counter: store.getState().counter })
+    })
  }
  render() {
    const { counter } = this.state
    return (
      <div>
+        <h3>App Counter: {counter}</h3>
        <div className="main">
          <Home />
          <Profile />
        </div>
      </div>
    )
  }
}

export default App

4、修改Store数据

constants.js

js
export const ADD_COUNTER = 'add_counter'
export const SUB_COUNTER = 'sub_counter'

actionCreators.js

js
import { ADD_COUNTER, SUB_COUNTER } from './constants'

+ export function addCounter(counter) {
  return {
    type: ADD_COUNTER,
    counter
  }
}
+ export function subCounter(counter) {
  return {
    type: SUB_COUNTER,
    counter
  }
}

调用action

js
  render() {
    const { counter } = this.state
    return (
      <div>
        <div>Home Counter: {counter}</div>
+        <button onClick={e => this.addCounter(1)}> +1 </button>
+        <button onClick={e => this.addCounter(10)}> +10 </button>
+        <button onClick={e => this.addCounter(100)}> +100 </button>
      </div>
    )
  }

  addCounter(counter) {
+    store.dispatch(addCounter(counter))
  }

react-redux使用

开始之前需要强调一下,redux和react没有直接的关系,你完全可以在React, Angular, Ember, jQuery, or vanilla JavaScript中使用Redux。

尽管这样说,redux依然是和React库结合的更好,因为他们是通过state函数来描述界面的状态,Redux可以发射状态的更新,让他们作出响应。

虽然我们之前已经实现了connectProvider这些帮助我们完成连接redux、react的辅助工具,但是实际上redux官方帮助我们提供了 react-redux 的库,可以直接在项目中使用,并且实现的逻辑会更加的严谨和高效。

使用步骤

1、安装react-redux:

sh
npm i react-redux
# 或者
yarn add react-redux

2、在全局中为整个项目提供store

js
  import { Provider } from 'react-redux'
  import store from './05-react-redux使用/store'

  const root =  ReactDom.createRoot(document.querySelector('#root'))
  root.render(
+    <Provider store={store}>
+      <App/>
+    </Provider>
  )

3、使用connect 连接store和组件

js
  import React, { PureComponent } from 'react'
  import { connect } from 'react-redux'

  import { addCounter } from '../store/actionCreator'

  export class Home extends PureComponent {
    render() {
      // 3. 解构props,获取state和action
+      const { counter, addCounter } = this.props
      return (
        <div>
          {/* 4. 展示state */}
+          <div>Home Counter: {counter}</div>
          {/* 5. 修改state */}
+          <button onClick={e => addCounter(1)}> +1 </button>
+          <button onClick={e => addCounter(10)}> +10 </button>
+          <button onClick={e => addCounter(100)}> +100 </button>
        </div>
      )
    }
  }

  // 2. 定义映射函数,映射state和action到组件的props上
+  const mapStateToProps = state => ({
    counter: state.counter,
  });
+  const mapDispatchToProps = dispatch => ({
    addCounter(counter) {
+      dispatch(addCounter(counter));
    },
  });

  // 1. 使用connect函数连接Home组件和store
+  export default connect(mapStateToProps, mapDispatchToProps)(Home)

react-redux源码导读

image-20230404111905366

Redux异步操作

组件中异步操作

在之前简单的案例中,redux中保存的counter是一个本地定义的数据

  • 我们可以直接通过同步的操作来dispatch action,state就会被立即更新。

  • 但是真实开发中,redux中保存的很多数据可能来自服务器,我们需要进行异步的请求,再将数据保存到redux中。

在之前学习网络请求的时候我们讲过,网络请求可以在class组件的componentDidMount中发送,所以我们可以有这样的结构:

image-20230404111934833

我现在完成如下案例操作:

  • 在Home组件中请求banners和recommends的数据;

  • 在Profile组件中展示banners和recommends的数据;

示例

image-20230414152218255

1、 发送异步网络请求,并派送action,修改state

js
import { connect } from 'react-redux'

import { getHomeMultidataAction } from '../store/actionCreator'

export class Home extends PureComponent {
  componentDidMount() {
    // 1. 发送异步网络请求
+    axios.get('http://123.207.32.32:8000/home/multidata').then(res => {
      const banners = res.data.data.banner.list
      const recommends = res.data.data.recommend.list
      this.props.getHomeMultidata({ banners, recommends })
    })
  }
  render() {
    return (
      <div>Home</div>
    )
  }
}

// 2. 派送action,修改state
+ const mapDispatchToProps = dispatch => ({
  getHomeMultidata(data) {
    dispatch(getHomeMultidataAction(data))
  }
})

export default connect(null, mapDispatchToProps)(Home)

2、生成action对象

js
// data格式: { banners: [], recommends: [] }
export function getHomeMultidataAction(data) {
  return {
    type: GET_HOMEMULTIDATA,
    data
  }
}

3、修改reducer,实现修改state

js
const initialState = {
  banners: [],
  recommends: []
}

export default function reducer(state = initialState, action) {
  switch (action.type) {
+    case GET_HOMEMULTIDATA:
+      return { ...state, banners: action.data.banners, recommends: action.data.recommends }
    default:
      return state
  }
}

4、渲染state中的数据到组件中

js
  import { connect } from 'react-redux'

  export class Profile extends PureComponent {
    render() {
+      const { banners, recommends } = this.props
      return (
        <div>
          <div>Profile</div>
          <hr />
          <h3>轮播图列表</h3>
          <ul>
            {
+              banners.map(item => {
+                return (
+                  <li key={item.acm}>{item.title}</li>
+                )
+              })
            }
          </ul>
          <hr />
          <h3>推荐列表</h3>
          <ul>
            {
+              recommends.map(item => (<li key={item.acm}>{item.title}</li>))
            }
          </ul>
        </div>
      )
    }
  }

+  const mapStateToProps = state => ({
+    banners: state.banners,
+    recommends: state.recommends
+  })
  
+  export default connect(mapStateToProps)(Profile)

redux中异步操作

上面的代码有一个缺陷

  • 我们必须将网络请求的异步代码放到组件的生命周期中来完成;

  • 事实上,网络请求到的数据也属于我们状态管理的一部分,更好的一种方式应该是将其也交给redux来管理;

image-20230404111953021

但是在redux中如何可以进行异步操作呢?

  • 答案就是使用中间件(Middleware)

  • 学习过Express或Koa框架的童鞋对中间件的概念一定不陌生;

  • 在这类框架中,Middleware可以帮助我们在请求和响应之间嵌入一些操作的代码,比如cookie解析、日志记录、文件压缩等操作;

理解中间件

redux也引入了中间件(Middleware)的概念:

  • 这个中间件的目的在dispatch的action和最终达到的reducer之间,扩展一些自己的代码

  • 比如日志记录调用异步接口添加代码调试功能等等;

我们现在要做的事情就是发送异步的网络请求,所以我们可以添加对应的中间件:

  • 这里官网推荐的、包括演示的网络请求的中间件是使用 redux-thunk;

redux-thunk是如何做到让我们可以发送异步的请求呢?

  • 我们知道,默认情况下的dispatch(action),action需要是一个JavaScript的对象;

  • redux-thunk可以让dispatch(action函数),action可以是一个函数;

  • 该函数会被调用,并且会传给这个函数一个dispatch函数和getState函数;

    • dispatch函数用于我们之后再次派发action;

    • getState函数考虑到我们之后的一些操作需要依赖原来的状态,用于让我们可以获取之前的一些状态;

如何使用redux-thunk

1、安装redux-thunk

sh
npm i redux-thunk
# 或者
yarn add redux-thunk

2、在创建store时传入应用了middleware的enhance函数

  • 通过applyMiddleware来结合多个Middleware, 返回一个enhancer;

  • 将enhancer作为第二个参数传入到createStore中;

js
+  import { createStore, applyMiddleware } from "redux";
+  import thunkMiddleWare from 'redux-thunk'
  import counterReducer from './reducer'

+  const store = createStore(counterReducer, applyMiddleware(thunkMiddleWare))

  export default store

3、定义返回一个函数的action:

  • 注意:这里不是返回一个对象了,而是一个函数;

  • 该函数在dispatch之后会被执行;

js
  // data格式: { banners: [], recommends: [] }
  export function getHomeMultidataAction(data) {
    return {
      type: GET_HOMEMULTIDATA,
      data
    }
  }

  // 发送网络请求
+  export function fetchHomeMultidataAction(data) {
+    return dispatch => {
      axios.get('http://123.207.32.32:8000/home/multidata').then(res => {
        const banners = res.data.data.banner.list
        const recommends = res.data.data.recommend.list
+        dispatch(getHomeMultidataAction({ banners, recommends }))
      })
    }
  }

4、在组件中发起异步请求

js
  export class Detail extends PureComponent {
    componentDidMount() {
+      this.props.fetchHomeMultidata()
    }
    render() {
      const { banners, recommends } = this.props
      ...省略
    }
  }
  const mapStateToProps = state => ({
    banners: state.banners,
    recommends: state.recommends
  })
  const mapDisPatchToProps = dispatch => ({
    fetchHomeMultidata() {
+      dispatch(fetchHomeMultidataAction())
    }
  })

  export default connect(mapStateToProps, mapDisPatchToProps)(Detail)

redux-devtools

我们之前讲过,redux可以方便的让我们对状态进行跟踪和调试,那么如何做到呢?

  • redux官网为我们提供了redux-devtools的工具;

  • 利用这个工具,我们可以知道每次状态是如何被修改的,修改前后的状态变化等等;

安装该工具需要两步:

  • 第一步:在对应的浏览器中安装相关的插件(比如Chrome浏览器扩展商店中搜索Redux DevTools即可);

  • 第二步:在redux中继承devtools的中间件;

js
  import { createStore, applyMiddleware, compose } from "redux";
  import thunkMiddleWare from 'redux-thunk'
  import counterReducer from './reducer'

  // 配置redux-devtools
+  const composeEnhancer = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose
+  const store = createStore(counterReducer, composeEnhancer(applyMiddleware(thunkMiddleWare)))

  export default store

开启trace:

window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({trace: true})

js
  import { createStore, applyMiddleware, compose } from "redux";
  import thunkMiddleWare from 'redux-thunk'
  import counterReducer from './reducer'

  // 配置redux-devtools
+  const composeEnhancer = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({trace: true}) || compose
  const store = createStore(counterReducer, composeEnhancer(applyMiddleware(thunkMiddleWare)))

  export default store

Reducer模块拆分

Reducer代码拆分

我们先来理解一下,为什么这个函数叫reducer?

我们来看一下目前我们的reducer:

  • 当前这个reducer既有处理counter的代码,又有处理home页面的数据;

  • 后续counter相关的状态或home相关的状态会进一步变得更加复杂;

  • 我们也会继续添加其他的相关状态,比如购物车、分类、歌单等等;

  • 如果将所有的状态都放到一个reducer中进行管理,随着项目的日趋庞大,必然会造成代码臃肿、难以维护。

因此,我们可以对reducer进行拆分:

  • 我们先抽取一个对counter处理的reducer;

  • 再抽取一个对home处理的reducer;

  • 将它们合并起来;

Reducer文件拆分

目前我们已经将不同的状态处理拆分到不同的reducer中,我们来思考:

  • 虽然已经放到不同的函数了,但是这些函数的处理依然是在同一个文件中,代码非常的混乱;

  • 另外关于reducer中用到的constant、action等我们也依然是在同一个文件中;

image-20230404112214180

1、home/constants.js

js
export const GET_HOMEMULTIDATA = 'get_home_multidata'

2、home/actionCreator.js

js
import axios from 'axios'
import { GET_HOMEMULTIDATA } from './constants'

// data格式: { banners: [], recommends: [] }
export function getHomeMultidataAction(data) {
  console.log(data)
  return {
    type: GET_HOMEMULTIDATA,
    data
  }
}

// 发送网络请求
export function fetchHomeMultidataAction(data) {
  return dispatch => {
    axios.get('http://123.207.32.32:8000/home/multidata').then(res => {
      const banners = res.data.data.banner.list
      const recommends = res.data.data.recommend.list
      dispatch(getHomeMultidataAction({ banners, recommends }))
    })
  }
}

3、home/reducer.js

js
import { GET_HOMEMULTIDATA } from './constants'

const initialState = {
  banners: [],
  recommends: []
}

export default function reducer(state = initialState, action) {
  switch (action.type) {
    case GET_HOMEMULTIDATA:
      console.log('reducer: ', state)
      return { ...state, banners: action.data.banners, recommends: action.data.recommends }
    default:
      return state
  }
}

4、home/index.js

js
import reducer from './reducer'
export default reducer

export * from './actionCreator'

combineReducers函数

目前我们合并的方式是通过每次调用reducer函数自己来返回一个新的对象。

事实上,redux给我们提供了一个combineReducers函数可以方便的让我们对多个reducer进行合并

js
  import { createStore, applyMiddleware, compose, combineReducers } from "redux";
  import thunkMiddleWare from 'redux-thunk'
  import counterReducer from './counter'
  import homeReducer from './home'
   
  // 合并reducer
+  const reducer = combineReducers({
+    counter: counterReducer,
+    home: homeReducer
+  })

  // 配置redux-devtools
  const composeEnhancer = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({trace: true}) || compose
  const store = createStore(reducer, composeEnhancer(applyMiddleware(thunkMiddleWare)))

  export default store

注意: 此时的store结构类似如下:

js
{
  counter: {
      counter: 100
  },
  home: {
      banners: [],
  	  recommends: []
  }
}

取值时要注意添加 counterhome

那么combineReducers是如何实现的呢?

  • 事实上,它也是将我们传入的reducers合并到一个对象中,最终返回一个combination的函数(相当于我们之前的reducer函数了);

  • 在执行combination函数的过程中,它会通过判断前后返回的数据是否相同来决定返回之前的state还是新的state;

  • 新的state会触发订阅者发生对应的刷新,而旧的state可以有效的组织订阅者发生刷新;

底层原理:combineReducers

image-20230411163117735

ReduxToolkit

RTK-介绍

Redux Toolkit 是官方推荐的编写 Redux 逻辑的方法。

  • 在前面我们学习Redux的时候应该已经发现,redux的编写逻辑过于的繁琐和麻烦。

  • 并且代码通常分拆在多个文件中(虽然也可以放到一个文件管理,但是代码量过多,不利于管理);

  • Redux Toolkit包旨在成为编写Redux逻辑的标准方式,从而解决上面提到的问题;

  • 在很多地方为了称呼方便,也将之称为“RTK”;

RTK-安装

sh
npm install @reduxjs/toolkit react-redux

API

  • configureStore({ reducer, middleware, devTools, ... })返回:store,包装createStore以提供简化的配置选项和良好的默认值。它可以自动组合你的 slice reducer,添加你提供的任何 Redux 中间件,redux-thunk默认包含,并启用 Redux DevTools Extension。
    • 参数
    • reducer:``,Redux store 的根 reducer
    • middleware:``,要使用的中间件数组
    • devTools:``,是否启用开发工具(如 Redux DevTools),默认true
    • preloadedState:``,初始状态
    • enhancers:``,其他 store 增强器
    • 返回
    • store:``,返回的是一个 Redux store 实例,而不是一个类。因此无法创建多个 store 实例
  • createSlice({ name, initialState, reducers,... })返回:reducerSlice,用于创建一个Redux reducer和action creator的集合
    • 参数
    • nameString,用于标识这个reducer的名称,action.type会根据name生成
    • initialStateany,表示这个reducer的初始状态
    • reducers{ reducer,... },用于定义这个reducer的action creator和对应的reducer函数
      • reducer(state, action) => void,相当于之前的reducer函数
    • 返回
    • reducerSlice:``,返回一个reducer片段
  • createAsyncThunk(typePrefix, payloadCreator, options? )返回:,用于创建一个异步action creator
    • 参数
    • typePrefixString,用于标识这个异步action creator的类型前缀
    • payloadCreator(arg, thunkAPI) => Promise,用于处理异步操作并返回一个Promise对象
    • options?Object,用于配置异步action creator的一些选项
      • fulfilled:用于指定异步操作成功时的处理函数。
      • rejected:用于指定异步操作失败时的处理函数。
      • pending:用于指定异步操作进行中时的处理函数。
      • dispatchCondition:用于指定在什么条件下才会dispatch这个action的函数。
      • condition:用于指定在什么条件下才会调用payloadCreator函数的函数。
      • typeSuffixes:用于指定异步action creator的类型后缀的对象。
      • serializeError:用于指定如何序列化异步操作的错误信息的函数。

RTK-基本使用

1、创建store:configureStore()

store/index.js

js
  import { configureStore } from '@reduxjs/toolkit'
  import counterReducer from './features/counter'
  import homeReducer from './features/home'

+  const store = configureStore({
+    reducer: {
+      counter: counterReducer,
+      home: homeReducer
+    }
+  })

  export default store

2、创建reducer片段:createSlice()

couterReducer

js
  import { createSlice } from "@reduxjs/toolkit";

+  const counterSlice = createSlice({
+    name: 'counter',
+    initialState: {
+      counter: 100
+    },
+    reducers: {
+      addCounter(state, action) {
+
+      },
+      subCounter(state, action) {
+        
+      }
+    }
+  })

+  export default counterSlice.reducer

3、结合redux和react组件

  1. 提供store到App组件

index.js

js
import ReactDom from 'react-dom/client'
import App from './09-reduxToolkit/App'

+ import { Provider } from 'react-redux'
+ import store from './09-reduxToolkit/store'


const root =  ReactDom.createRoot(document.querySelector('#root'))
root.render(
+  <Provider store={store}>
    <App/>
+  </Provider>
)
  1. 获取store中的数据

App.jsx

js
  import Home from './cpns/Home'
  import Profile from './cpns/Profile'
  import { connect } from 'react-redux'

  export class App extends PureComponent {
    render() {
+      const { counter } = this.props
      return (
        <div>
+          <h3>App Counter: {counter}</h3>
          <div className="pages">
            <Home />
            <Profile />
          </div>
        </div>
      )
    }
  }

+  const mapStateToProps = state => ({
+    counter: state.counter.counter
+  })

+  export default connect(mapStateToProps)(App)
  1. 修改store中的数据

    1. 导出actions

      js
        import { createSlice } from "@reduxjs/toolkit";
      
        const counterSlice = createSlice({
          name: 'counter',
          initialState: {
            counter: 100
          },
          reducers: {
      +      addCounterAction(state, { payload }) {
              state.counter += payload
            },
      +      subCounterAction(state, { payload }) {
              state.counter -= payload
            }
          }
        })
      
      +  export const { addCounterAction, subCounterAction } = counterSlice.actions
        export default counterSlice.reducer
    2. dispatch导出的actions

      js
        import { addCounterAction } from '../store/features/counter'
      
        export class Home extends PureComponent {
          render() {
      +      const { counter, addCounter } = this.props
            return (
              <div>
                <div>Home Counter: {counter}</div>
      +          <button onClick={e => addCounter(1)}> +1 </button>
      +          <button onClick={e => addCounter(10)}> +10 </button>
      +          <button onClick={e => addCounter(100)}> +100 </button>
              </div>
            )
          }
        }
        const mapDispatchToProps = dispatch => ({
      +    addCounter(num) {
      +      dispatch(addCounterAction(num))
      +    }
        })
        export default connect(mapStateToProps, mapDispatchToProps)(Home)

RTK-异步操作

在之前的开发中,我们通过redux-thunk中间件让dispatch中可以进行异步操作。

Redux Toolkit默认已经给我们集成了Thunk相关的功能createAsyncThunk

js
import { createAsyncThunk } from "@reduxjs/toolkit";

export const fetchHomeMultidataAction = createAsyncThunk('home/multidata', async () => {
  const res = await axios.get('http://123.207.32.32:8000/home/multidata')
  return res.data.data
})

当createAsyncThunk创建出来的action被dispatch时,会存在三种状态:

  • pending:action被发出,但是还没有最终的结果;

  • fulfilled:获取到最终的结果(有返回值的结果);

  • rejected:执行过程中有错误或者抛出了异常;

我们可以在createSlice的extraReducer中监听这些结果:

image-20230404112528739

示例:异步发送网络请求

1、创建homeSlice

js
// store/features/home.js

  import { createSlice } from "@reduxjs/toolkit";

+  const homeSlice = createSlice({
+    name: 'home',
+    initialState: {
      banners: [],
      recommends: []
    }
  })

+  export default homeSlice.reducer

2、添加片段到store

js
  import { configureStore } from '@reduxjs/toolkit'

  import counterReducer from '../store/features/counter'
+  import homeReducer from '../store/features/home'

+  const store = configureStore({
+    reducer: {
      counter: counterReducer,
+      home: homeReducer
    }
  })

  export default store

3、展示home数据

js
  export class Profile extends PureComponent {
    render() {
+      const { banners, recommends } = this.props
      return (
        <div>
          <h3>Profile</h3>
          <div>发送异步请求</div>
          <div className='lists'>
            <div>
              <h4>轮播图列表</h4>
              <ul>
                {
+                  banners.map((item, index) => {
                    return <li key={item.acm}>{item.title}</li>
                  })
                }
              </ul>
            </div>
            <div>
              <h4>推荐列表</h4>
              <ul>
                {
+                  recommends.map((item, index) => {
                    return <li key={item.acm}>{item.title}</li>
                  })
                }
              </ul>
            </div>
          </div>

        </div>
      )
    }
  }

+  const mapStateToProps = state => ({
    banners: state.home.banners,
    recommends: state.home.recommends
  })

  export default connect(mapStateToProps, mapDispatchToProps)(Profile)

4.1、在组件中发送网络请求(不推荐)

image-20230414180259036

4.2、在redux中发送网络请求(推荐

在store中定义 fetchHomeMultidataAction,发送网络请求

js
  import axios from 'axios'

  import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";

+  export const fetchHomeMultidataAction = createAsyncThunk('home/multidata', async () => {
+    const res = await axios.get('http://123.207.32.32:8000/home/multidata')
+    return res.data.data
+  })

  const homeSlice = createSlice({
    name: 'home',
    initialState: {
      banners: [],
      recommends: []
    },
    extraReducers: {
+      [fetchHomeMultidataAction.pending](state, { payload }) {
        console.log('fetchHomeMultidataAction.pending')
      },
+      [fetchHomeMultidataAction.fulfilled](state, { payload }) {
+        console.log(payload)
+        state.banners = payload.banner.list
+        state.recommends = payload.recommend.list
+      },
+      [fetchHomeMultidataAction.rejected](state, { payload }) {
        console.log('fetchHomeMultidataAction.rejected')
      }
    }
    }
  })

  export default homeSlice.reducer

在组件中调用 fetchHomeMultidataAction

js
import { fetchHomeMultidataAction } from '../store/features/home'

  export class Profile extends PureComponent {
+    componentDidMount() {
+      this.props.getHomeMultidata()
+    }
    render() {
+      const { banners, recommends } = this.props
      return (【...省略】)
    }
  }

  const mapStateToProps = state => ({
    banners: state.home.banners,
    recommends: state.home.recommends
  })
  const mapDispatchToProps = dispatch => ({
    getHomeMultidata() {
+      dispatch(fetchHomeMultidataAction())
    }
  })

  export default connect(mapStateToProps, mapDispatchToProps)(Profile)

extraReducer另二种写法

写法2: extraReducer还可以传入一个函数,函数接受一个builder参数。

  • 我们可以向builder中添加case来监听异步操作的结果:
js
+  export const fetchHomeMultidataAction = createAsyncThunk('home/multidata', async () => {
    const res = await axios.get('http://123.207.32.32:8000/home/multidata')
    return res.data.data
  })

  const homeSlice = createSlice({
    name: 'home',
    initialState: {
      banners: [],
      recommends: []
    },
+    extraReducers(builder) {
+      builder
+        .addCase(fetchHomeMultidataAction.fulfilled, (state, { payload }) => {
          state.banners = payload.banner.list
          state.recommends = payload.recommend.list
        })
+        .addCase(fetchHomeMultidataAction.rejected, (state) => {
          console.log('fetchHomeMultidataAction.rejected')
        })
    }
  })

写法3: 直接在createAsyncThunk中dispatch

js
  import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";

  export const fetchHomeMultidataAction = createAsyncThunk('home/multidata', async (payload, { dispatch }) => {
    const res = await axios.get('http://123.207.32.32:8000/home/multidata')
    
    const banners = res.data.data.banner.list
    const recommends = res.data.data.recommend.list
    // 直接在此处dispatch reducers中的action,dispatch是通过参数传递过来的
+    dispatch(setBannersAction(banners))
+    dispatch(setRecommendsAction(recommends))
  })

  const homeSlice = createSlice({
    name: 'home',
    initialState: {
      banners: [],
      recommends: []
    },
    reducers: {
+      setBannersAction(state, { payload }) {
        state.banners = payload
      },
+      setRecommendsAction(state, { payload }) {
        state.recommends = payload
      }
    }
  })

在组件中发送异步请求

js
  const mapDispatchToProps = dispatch => ({
    getHomeMultidata() {
+      dispatch(fetchHomeMultidataAction())
    }
  })

  export default connect(mapStateToProps, mapDispatchToProps)(Profile)

注意: 如果想要捕获createAsyncThunk中的rejected状态,需要在回调中使用try...catch包裹发送异步请求的代码

RTK-数据不可变性原理

在React开发中,我们总是会强调数据的不可变性:

  • 无论是类组件中的state,还是redux中管理的state;

  • 事实上在整个JavaScript编码过程中,数据的不可变性都是非常重要的;

React中实现数据不可变性的方法有以下几种:

方法一:浅拷贝

  • 展开运算符:``,可以用于数组、对象等数据结构的浅拷贝,避免直接修改原始数据。
  • Array.prototype.concat():``,可以将多个数组合并成一个新的数组,也可以用于数组的浅拷贝。
  • Object.assign():``,可以将多个对象合并成一个新的对象,也可以用于对象的浅拷贝。

浅拷贝的缺点

  • 过大的对象,进行浅拷贝也会造成性能的浪费

  • 浅拷贝后的对象,在深层改变时,依然会对之前的对象产生影响

方法二:使用不可变数据结构

  • Immutable.js:``,
  • Immer.js:``,

使用Immutable.js或者Immer.js等第三方库,这些库提供了一些不可变的数据结构,如List、Map、Set等,可以方便地进行数据的不可变性操作。

事实上Redux Toolkit底层使用了immerjs的一个库来保证数据的不可变性。

在我们公众号的一片文章中也有专门讲解immutable-js库的底层原理和使用方法:

https://mp.weixin.qq.com/s/hfeCDCcodBCGS5GpedxCGg

image-20240719173023257

为了节约内存,又出现了一个新的算法:Persistent Data Structure(持久化数据结构或一致性数据结构);

  • 用一种数据结构来保存数据;

  • 当数据被修改时,会返回一个对象,但是新的对象会尽可能的利用之前的数据结构而不会对内存造成浪费;

connect高阶组件

自定义connect函数

手写 connect 高阶函数

1、基础功能:展示state

js
import { PureComponent } from "react"

import store from '../store'

// 手写高阶组件connect
function mrConnect(mapStateToProps, mapDispatchToProps) {
  return function(Cpn) {
    return class extends PureComponent {
      render() {
        return <Cpn {...this.props} {...mapStateToProps(store.getState())} {...mapDispatchToProps(store.dispatch)} />
      }
    }
  }
}

export default mrConnect

2、订阅store:监听store数据变化,重新渲染组件

js
import { PureComponent } from "react"

import store from '../store'

// 手写高阶组件connect
function mrConnect(mapStateToProps, mapDispatchToProps) {
  return function(Cpn) {
    return class extends PureComponent {
      constructor(props) {
        super(props)
+        this.state = mapStateToProps(store.getState())
      }
      componentDidMount() {
+        this.unsubscribe = store.subscribe(() => {
+          this.setState(mapStateToProps(store.getState()))
+        })
      }
      componentWillUnmount() {
+        this.unsubscribe()
      }
      render() {
        return <Cpn {...this.props} {...mapStateToProps(store.getState())} {...mapDispatchToProps(store.dispatch)} />
      }
    }
  }
}

export default mrConnect

3、使用 connect 高阶函数

js
const mapStateToProps = state => ({
  counter: state.counter.counter
})
const mapDispatchToProps = dispatch => ({
  addCounter(num) {
    dispatch(addCounterAction(num))
  },
  subCounter(num) {
    dispatch(subCounterAction(num))
  }
})

+ export default mrConnect(mapStateToProps, mapDispatchToProps)(App)

context处理store

但是上面的connect函数有一个很大的缺陷:依赖导入的store

  • 如果我们将其封装成一个独立的库,需要依赖用于创建的store,我们应该如何去获取呢?

  • 难道让用户来修改我们的源码吗?不太现实;

正确的做法是我们提供一个Provider,Provider来自于我们创建的Context,让用户将store传入到value中即可;

4、解耦store

4.1、创建 StoreContext

js
// hoc\storeContext.js
import { createContext } from "react";

const StoreContext = createContext()

export default StoreContext

4.2、提供store到根元素App上

js
// index.js

root.render(
  <StoreContext.Provider value={store} >
    <App/>
  </StoreContext.Provider>
)

4.3、在高阶组件mrConnect 中使用 StoreContext传递的store

js
  import { PureComponent } from "react"

+  import StoreContext from "./storeContext"

  // 手写高阶组件connect
  function mrConnect(mapStateToProps, mapDispatchToProps) {
    return function(Cpn) {
      class newCpn extends PureComponent {
+        constructor(props, context) {
          super(props)
          this.state = {
+            storeState: context.getState()
          }
        }
        componentDidMount() {
+          this.unsubscribe = this.context.subscribe(() => {
            this.setState({ storeState: this.context.getState() })
          })
        }
        componentWillUnmount() {
          this.unsubscribe()
        }
        render() {
+          return <Cpn {...this.props} {...mapStateToProps(this.context.getState())} {...mapDispatchToProps(this.context.dispatch)} />
        }
      }
+      newCpn.contextType = StoreContext

      return newCpn
    }
  }

  export default mrConnect

中间件的实现原理

打印日志需求

前面我们已经提过,中间件的目的是在redux中插入一些自己的操作:

  • 比如我们现在有一个需求在dispatch之前,打印一下本次的action对象dispatch完成之后可以打印一下最新的store state

  • 也就是我们需要将对应的代码插入到redux的某部分,让之后所有的dispatch都可以包含这样的操作;

方法一:在派发的前后进行相关的打印

如果没有中间件,我们是否可以实现类似的代码呢? 可以在派发的前后进行相关的打印。

但是这种方式缺陷非常明显:

  • 首先,每一次的dispatch操作,我们都需要在前面加上这样的逻辑代码;

  • 其次,存在大量重复的代码,会非常麻烦和臃肿;

是否有一种更优雅的方式来处理这样的相同逻辑呢?

方法二:将代码封装到一个独立的函数中

  • 我们可以将代码封装到一个独立的函数中

但是这样的代码有一个非常大的缺陷:

  • 调用者(使用者)在使用我的dispatch时,必须使用我另外封装的一个函数dispatchAndLog;

  • 显然,对于调用者来说,很难记住这样的API,更加习惯的方式是直接调用dispatch;

修改dispatch

事实上,我们可以利用一个hack一点的技术:Monkey Patching,利用它可以修改原有的程序逻辑;

我们对代码进行如下的修改:

  • 这样就意味着我们已经直接修改了dispatch的调用过程;

  • 在调用dispatch的过程中,真正调用的函数其实是dispatchAndLog;

当然,我们可以将它封装到一个模块中,只要调用这个模块中的函数,就可以对store进行这样的处理:

store/index.js

js
// 拦截每次的dispatch,打印消息
function log(store) {
  // 1. 保存原始dispatch函数到变量中
  const next = store.dispatch
  // 2. dispatch函数的增强
  function dispatchAndLog(action) {
    console.log('action: ', action)
    next(action)
    console.log('state: ', store.getState())
  }
  // 3. 给store.dispatch重新赋值一个函数
  store.dispatch = dispatchAndLog
}
log(store)

thunk需求

redux-thunk的作用:

  • 我们知道redux中利用一个中间件redux-thunk可以让我们的dispatch不再只是处理对象,并且可以处理函数;

  • 那么redux-thunk中的基本实现过程是怎么样的呢?事实上非常的简单。

我们来看下面的代码:

  • 我们又对dispatch进行转换,这个dispatch会判断传入的
js
// 手写中间件:mrThunk
function mrThunk(store) {
  const next = store.dispatch

  function dispatchAndThunk(action) {
    if(typeof action === 'function') {
      //此处传递store.dispatch这个新的dispatch,防止在dispatch的函数中又dispatch了一个
      action(store.dispatch, store.getState)
    } else if(typeof action === 'object') {
      next(action)
    }
  }

  store.dispatch = dispatchAndThunk
}
mrThunk(store)

store中返回的action如下:

js
  // 发送网络请求
  export function fetchHomeMultidataAction(data) {
    return dispatch => {
      axios.get('http://123.207.32.32:8000/home/multidata').then(res => {
        const banners = res.data.data.banner.list
        const recommends = res.data.data.recommend.list
        dispatch(getHomeMultidataAction({ banners, recommends }))
      })
    }
  }

合并中间件

单个调用某个函数来合并中间件并不是特别的方便,我们可以封装一个函数来实现所有的中间件合并:

js
// 手写合并中间件:mrApplyMiddleware
function mrApplyMiddleware(store, ...middlewares) {
  middlewares.forEach(middleware => {
    middleware(store)
  })
}
mrApplyMiddleware(store, mrLog, mrThunk)

我们来理解一下上面操作之后,代码的流程:

image-20230415111805477

当然,真实的中间件实现起来会更加的灵活,这里我们仅仅做一个抛砖引玉,有兴趣可以参考redux合并中间件的源码流程。

React状态管理选择

React中的state如何管理

我们学习了Redux用来管理我们的应用状态,并且非常好用(当然,你学会前提下,没有学会,好好回顾一下)。

目前我们已经主要学习了三种状态管理方式

  • 方式一:组件中自己的state管理;

  • 方式二:Context数据的共享状态;

  • 方式三:Redux管理应用状态;

在开发中如何选择呢?

  • 首先,这个没有一个标准的答案;

  • 某些用户,选择将所有的状态放到redux中进行管理,因为这样方便追踪和共享;

  • 有些用户,选择将某些组件自己的状态放到组件内部进行管理;

  • 有些用户,将类似于主题、用户信息等数据放到Context中进行共享和管理;

  • 做一个开发者,到底选择怎样的状态管理方式,是你的工作之一,可以一个最好的平衡方式(Find a balance that works for you, and go with it.);

Redux的作者有给出自己的建议:

image-20230404112914790

目前项目中我采用的state管理方案:

  • UI相关的组件内部可以维护的状态,在组件内部自己来维护;

  • 大部分需要共享的状态,都交给redux来管理和维护;

  • 服务器请求的数据(包括请求的操作),交给redux来维护;

当然,根据不同的情况会进行适当的调整,在后续学习项目实战时,我也会再次讲解以实战的角度来设计数据的管理方案。